#include "Scene.h"
#include <limits>
#include <algorithm>

namespace RayTracer
{
    float Scene::tMin = -5.0f;
    float Scene::tMax = 5.0f;
	float Scene::epsilon = 0.0001f;

    Scene::Scene() : recursionDepth(0), viewPlane(ViewPlane(Perspective))
    {
    }

    Scene::~Scene()
    {
		for(size_t i = 0; i < this->surfaces.size(); ++i)
		{
			delete this->surfaces[i];
		}
		this->surfaces.clear();

		for(size_t i = 0; i < this->lights.size(); ++i)
		{
			delete this->lights[i];
		}
		this->lights.clear();
    }

    void Scene::AddSurface(ISurface *surface)
    {
		this->surfaces.push_back(surface);
    }

    void Scene::AddLight(Light3D *light)
    {
		this->lights.push_back(light);
    }

    HitInfo Scene::GetIntersectedSurface(const Ray3D *ray) const
    {
		HitInfo res;
		res.distance = tMax - tMin;

		for(size_t i = 0; i < this->surfaces.size(); ++i)
		{
			ISurface *surface = this->surfaces[i];

			HitInfo tmp = surface->Hit(ray, Scene::epsilon, tMax);
			if(tmp.res == HitInfo::Miss)
			{
				continue;
			}
			
			if(tmp.distance < res.distance)
			{
				res = tmp;
			}
		}

		return res;
    }

	ISurface *lastSurface = 0;
	ISurface *currentSurface = 0;

    Color Scene::GetPixelColor(int x, int y)
    {
		if(lastSurface != currentSurface)
		{
			Color result;
			lastSurface = currentSurface;
			for(int i = -1; i < 2; ++i)
			{
				for(int j = -1; j < 2; ++j)
				{
					this->recursionDepth = 0;
					Ray3D viewRay = this->viewPlane.GetViewRay((float)(x + i), (float)(y + j));
					result = Color::Add(&this->RayTrace(&viewRay, 1.0f), &result);
				}
			}

			return Color::Multiply(&result, 1.0f / 9.0f);
		}
		else
		{
			Ray3D viewRay = this->viewPlane.GetViewRay((float)x, (float)y);
			this->recursionDepth = 0;
			return this->RayTrace(&viewRay, 1.0f);
		}
    }

	Color Scene::RayTrace(const Ray3D *ray, float localRefrIndex)
	{
		if(this->recursionDepth == Scene::maxRecursionDepth)
		{
			return Color();
		}

		this->recursionDepth++;

		HitInfo closestHitInfo = this->GetIntersectedSurface(ray);

		if(closestHitInfo.res == HitInfo::Miss)
		{
			currentSurface = 0;
			return Color();
		}

		ISurface *closestSurface = closestHitInfo.surface;
		currentSurface = closestSurface;
		Color finalColor;

		for(size_t i = 0; i < this->lights.size(); ++i)
		{
			Vector3D dir = Vector3D::Subtract(&this->lights[i]->position, &closestHitInfo.hitPoint);
			Vector3D::Normalize(&dir);
			Vector3D shadowOrigin = Vector3D::Add(&closestHitInfo.hitPoint, &Vector3D::MultiplyN(&dir, Scene::epsilon));
			Ray3D shadowRay(shadowOrigin, dir);

			if(this->RayIntersectsSurface(&shadowRay, closestHitInfo.surface))
			{
				continue;
			}

			Color closestColor = closestSurface->GetColor(this->lights[i], &closestHitInfo.hitPoint);
			finalColor = Color::Add(&closestColor, &finalColor);
		}

		float surfaceReflection = closestSurface->GetMaterial()->GetReflection();
		Color reflectedColor;

		if(surfaceReflection > 0.0f)
		{
			Vector3D surfaceNormal = closestSurface->Normal(&closestHitInfo.hitPoint);
			Vector3D reflectedDir = Vector3D::Subtract(&ray->direction, &Vector3D::MultiplyN(&surfaceNormal, 2.0f * DOT(ray->direction, surfaceNormal)));
			Ray3D reflectedRay(Vector3D::Add(&closestHitInfo.hitPoint, &Vector3D::MultiplyN(&reflectedDir, Scene::epsilon)), reflectedDir);
			reflectedColor = this->RayTrace(&reflectedRay, localRefrIndex);
		}

		float refraction = closestSurface->GetMaterial()->GetRefraction();
		Color refractedColor;

		if(refraction > 0.0f)
		{
			float refrIndex = closestSurface->GetMaterial()->GetRefractiveIndex();
			float n = localRefrIndex / refrIndex;
			Vector3D normal = closestSurface->Normal(&closestHitInfo.hitPoint);
			Vector3D::Multiply(&normal, (float)closestHitInfo.res);
			float cosI = -DOT(normal, ray->direction);
			float cosT2 = 1.0f - n * n * (1.0f - cosI * cosI);
			if(cosT2 > 0.0f)
			{
				Vector3D tmp1 = Vector3D::MultiplyN(&ray->direction, n);
				float tmp2 = n * cosI - sqrtf(cosT2);
				tmp1 = Vector3D::MultiplyN(&Vector3D::Add(&tmp1, tmp2), &normal);

				Vector3D normalizedTmp1 = tmp1;
				Vector3D::Normalize(&normalizedTmp1);
				Ray3D refractedRay = Ray3D(Vector3D::Add(&closestHitInfo.hitPoint, &Vector3D::MultiplyN(&tmp1, Scene::epsilon)), normalizedTmp1);
				refractedColor = this->RayTrace(&refractedRay, refrIndex);
			}
		}

		return Color::Add(&finalColor, &Color::Add(&Color::Multiply(&reflectedColor, surfaceReflection), &refractedColor));
	}
		
    void Scene::Resize(float w, float h)
    {
		this->viewPlane.Resize(w, h);
    }

	bool Scene::RayIntersectsSurface(Ray3D *ray, ISurface *surfaceToSkip)
	{
		float tMax = Vector3D::Length(&ray->direction);
		Vector3D::Normalize(&ray->direction);
		for(size_t i = 0; i < this->surfaces.size(); ++i)
		{
			ISurface *current = this->surfaces[i];
			if(current == surfaceToSkip)
			{
				continue;
			}

			HitInfo hitInfo = current->Hit(ray, 0.0f, tMax);
			if(hitInfo.res != HitInfo::Miss)
			{
				return true;
			}
		}

		return false;
	}
};
